
import { v4 as uuidv4} from 'uuid'
import { mat4, Mat4, quat, vec3, Vec3, vec4 } from 'wgpu-matrix';
import { PriorityLinkList } from '../common/priority-link-list';
import { SystemBase } from './system-base';

export type ComponentType = symbol

export class CaptureContext {
    private readonly capturedComponents: Record<symbol, Component[]> = {}

    public addCaptureType(componentType?: ComponentType) {
        if (!componentType)
            return

        this.capturedComponents[componentType] === undefined ? (this.capturedComponents[componentType] = []) : undefined
    }

    public getCapturedComponents(componentType?: ComponentType): Component[] | undefined {
        if (!componentType)
            return

        return this.capturedComponents[componentType]
    }

    public tryCapture(component: Component) {
        const t = COMPONENT_TYPEOF(component)
        if (this.capturedComponents[t] !== undefined)
            this.capturedComponents[t].push(component)
    }
}

export class BlazeEngine {
    private static _engineInstance: BlazeEngine = null
    private _systemLinkList: PriorityLinkList<SystemBase> = new PriorityLinkList()

    private _currentScene: Scene
    private _needToSwitchScene: boolean = false
    private _sceneToSwitch: Scene

    static createEngine() {
        if (this._engineInstance === null)
            this._engineInstance = new BlazeEngine()

        return this._engineInstance
    }

    loadScene(scene: Scene) {
        if (this._currentScene === undefined) {
            this._currentScene = scene
        } else {
            this._needToSwitchScene = true
            this._sceneToSwitch = scene
        }
    }

    createScene(): Scene {
        return new SceneInternal()
    }

    tick(timeStamp: number) {
        const capCtx = new CaptureContext()

        for (const system of this._systemLinkList) {
            system.beforeTick?.call(system, capCtx)
        }

        const scene = this._currentScene as SceneInternal
        if (scene)
            scene.$$tick(capCtx)

        if (this._needToSwitchScene && this._sceneToSwitch) {
            this._needToSwitchScene = false
            this._currentScene = this._sceneToSwitch
            this._sceneToSwitch = undefined
        }
        
        for (const system of this._systemLinkList) {
            system.afterTick?.call(system, capCtx)
        }
    }

    addSystem<T extends SystemBase>(system: T, priority: number): T {
        this._systemLinkList.put(system, priority)
        return system
    }
}

export class Scene {
    public createGameObject(name: string): GameObject { return new GameObject() }
}

class SceneInternal extends Scene {
    public gameObjectArray: GameObject[] = []
    public gameObjectToDestroy: GameObject[] = [] 

    public createGameObject(name: string): GameObject {
        const go = new GameObjectInternal(name)

        this.gameObjectArray.push(go)
        Object.defineProperty(go, '_attachedScene', {
            value: this,
            configurable: false,
            writable: false
        })

        return go
    }

    $$destroyGameObject(go: GameObject) {
        this.gameObjectToDestroy.push(go)
    }

    $$tick(capCtx: CaptureContext) {
        for (const go of this.gameObjectArray) {
            (<GameObjectInternal>go).$$tick?.call(go, capCtx)
        }
    }
}

export class GameObject {
    public readonly transform = new Transform()
    public readonly name: string

    protected _components: Component[] = []
    protected _attachedScene: SceneInternal 

    constructor(name: string = "GameObject") {
        this.name = name
    }

    addComponent<T extends Component>(ctor: new () => T): T {
        const componentInstance = new ctor()

        Object.defineProperty(componentInstance, 'gameObject', {
            value: this,
            writable: false,
            configurable: false
        })

        componentInstance.awake?.call(componentInstance)

        this._components.push(componentInstance)
        
        return componentInstance
    }

    getComponent<T extends Component>(cls: T): T {
        for (const c of this._components) {
            if (c['__$$BLAZE_COMPONENT_TYPE_UUID'] === cls['__$$BLAZE_COMPONENT_TYPE_UUID']) {
                return <T>c
            }
        }
        return undefined
    }
}

class GameObjectInternal extends GameObject {
    $$tick(capCtx: CaptureContext) {
        for (const c of this._components) {
            c.update?.call(c)
            capCtx.tryCapture(c)
        }
    }
}

export class Component {
    protected readonly gameObject: GameObject

    protected destoryGameObject(o: GameObject) {
        (<SceneInternal>(<any>this.gameObject)._attachedScene).$$destroyGameObject(o)
    }

    awake?()
    start?()
    update?()
}

export function COMPONENT_CLASS_TYPE(cls: any): ComponentType | undefined {
    return cls['__$$BLAZE_COMPONENT_TYPE_UUID']
}

export function COMPONENT_TYPEOF(c: Component) {
    return COMPONENT_CLASS_TYPE(Object.getPrototypeOf(c).constructor)
}

export function BLAZE_COMPONENT<T>(cls: T): T {
    const uuid = uuidv4()
    
    cls['__$$BLAZE_COMPONENT_TYPE_UUID'] = Symbol('__BLAZE_COMPONENT__' + uuid)
    
    return cls
}

export function COMPONENT_TYPE_EQUALS(c1: any, c2: any): boolean {
    const s1 = c1['__$$BLAZE_COMPONENT_TYPE_UUID']
    const s2 = c2['__$$BLAZE_COMPONENT_TYPE_UUID']

    return (s1 !== undefined && s2 !== undefined && s1 === s2)
}

export class Transform {
    children: Transform[]
    parent: Transform = null

    localPosition: Vec3
    localScale: Vec3
    localEulerAngel: Vec3

    get localToWorldMatrix(): Mat4 {
        const euler = this.localEulerAngel

        const rotScale = mat4.multiply(
            mat4.fromQuat(quat.fromEuler(euler[0], euler[1], euler[2], 'zyx')), mat4.scaling(this.localScale))

        const trans = mat4.translation(this.localPosition)
        const model = mat4.multiply(trans, rotScale)
        
        if (this.parent !== null) {
            return mat4.multiply(this.parent.localToWorldMatrix, model)
        }
        
        return model
    }

    get worldPosition(): Vec3 {
        const lp = this.localPosition
        const v = vec4.transformMat4(vec4.create(lp[0], lp[1], lp[2], 1), this.localToWorldMatrix)
        return vec3.create(v[0], v[1], v[2])
    }
}
